/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.yongnuo.remoteconlight;

import android.app.Service;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;

import android.content.Context;
import android.content.Intent;
import android.location.Address;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

import com.yongnuo.bluetooth.SampleGattAttributes;

/**
 * Service for managing connection and data communication with a GATT server hosted on a
 * given Bluetooth LE device.
 */
public class BluetoothLeServiceV2 extends Service {
    private final static String TAG = "zbble";

    private BluetoothManager mBluetoothManager;
    private BluetoothAdapter mBluetoothAdapter;

    public static final int STATE_DISCONNECTED  = 0;
    public static final int STATE_DISCONNECTING = 1;
    public static final int STATE_CONNECTING    = 2;
    public static final int STATE_CONNECTED 	= 3;

    public final static String ACTION_GATT_CONNECTING =
            "com.example.bluetooth.le.ACTION_GATT_CONNECTING";
    public final static String ACTION_GATT_CONNECTED =
            "com.example.bluetooth.le.ACTION_GATT_CONNECTED";
    public final static String ACTION_GATT_DISCONNECTED =
            "com.example.bluetooth.le.ACTION_GATT_DISCONNECTED";
    public final static String ACTION_GATT_SERVICES_DISCOVERED =
            "com.example.bluetooth.le.ACTION_GATT_SERVICES_DISCOVERED";
    public final static String ACTION_DATA_AVAILABLE =
            "com.example.bluetooth.le.ACTION_DATA_AVAILABLE";
    public final static String EXTRA_DATA =
            "com.example.bluetooth.le.EXTRA_DATA";
    public final static String DEVICE_ADDRESS = 
	    "com.example.bluetooth.le.DEVICE_ADDRESS";

    public final static UUID UUID_HEART_RATE_MEASUREMENT =
            UUID.fromString(SampleGattAttributes.HEART_RATE_MEASUREMENT);

    
    class BluetoothDeviceInfoPool {
	class BleDeviceInfo {
	    public String mBluetoothDeviceAddress;
	    public BluetoothGatt mBluetoothGatt;
	    public int mConnectionState = STATE_DISCONNECTED;
	    
	    public BleDeviceInfo(String  address) {
		// TODO Auto-generated constructor stub
		this.mBluetoothDeviceAddress = address;
	    }
	}
	
	private ArrayList<BleDeviceInfo> deviceInfo = new ArrayList<BleDeviceInfo>();
	
	private BleDeviceInfo GetDeviceInfo(String BluetoothDeviceAddress)
	{
	    synchronized (deviceInfo) {
		for(BleDeviceInfo devinfo: deviceInfo)
		{
		    if(devinfo.mBluetoothDeviceAddress.equals(BluetoothDeviceAddress))
		    {
			return devinfo;
		    }
		}	
	    }
	    return null;
	}
	
	private BleDeviceInfo GetDeviceInfo(BluetoothGatt BluetoothGatt)
	{
	    synchronized (deviceInfo) {
		for(BleDeviceInfo devinfo: deviceInfo)
		{
		    if(devinfo.mBluetoothGatt.equals(BluetoothGatt))
		    {
			return devinfo;
		    }
		}	
	    }
	    return null;
	}
	
	public boolean AddDevice(String BluetoothDeviceAddress)
	{
	    if(GetDeviceInfo(BluetoothDeviceAddress) == null)
	    {
		synchronized (deviceInfo) {
		    return deviceInfo.add(new BleDeviceInfo(BluetoothDeviceAddress));
		}
	    }
	    
	    Log.d(TAG, "BluetoothDeviceInfoPool: device had in pool!");
	    return false;
	}	
	public boolean AddDevice(String BluetoothDeviceAddress, BluetoothGatt BluetoothGatt)
	{
	    if(GetDeviceInfo(BluetoothDeviceAddress) == null)
	    {
		BleDeviceInfo info  = new BleDeviceInfo(BluetoothDeviceAddress);
		info.mBluetoothGatt = BluetoothGatt;
		synchronized (deviceInfo) {
		    return deviceInfo.add(info);
		}
	    }
	    
	    Log.d(TAG, "BluetoothDeviceInfoPool: device had in pool!");
	    return false;
	}
	
	public void DelDevice(String BluetoothDeviceAddress)
	{
	    BleDeviceInfo devinfo = GetDeviceInfo(BluetoothDeviceAddress);
	    if(devinfo != null)
	    {
		synchronized (deviceInfo) {
		    deviceInfo.remove(devinfo);
		}
	    }
	}
	public void DelDevice(BluetoothGatt BluetoothGatt)
	{
	    BleDeviceInfo devinfo = GetDeviceInfo(BluetoothGatt);
	    if(devinfo != null)
	    {
		synchronized (deviceInfo) {
		    deviceInfo.remove(devinfo);
		}
	    }
	}	
	
	public boolean HasDevice(String BluetoothDeviceAddress)
	{
	    if(GetDeviceInfo(BluetoothDeviceAddress) == null)
	    {
		return false;
	    }
	    return true;
	}
	
	public String GetDeviceAddress(BluetoothGatt BluetoothGatt)
	{
	    BleDeviceInfo devinfo = GetDeviceInfo(BluetoothGatt);
	    if(devinfo != null)
	    {
		return devinfo.mBluetoothDeviceAddress;
	    }
	    
	    return null;
	}
	
	public BluetoothGatt GetDeviceGatt(String BluetoothDeviceAddress)
	{
	    BleDeviceInfo devinfo = GetDeviceInfo(BluetoothDeviceAddress);
	    if(devinfo != null)
	    {
		return devinfo.mBluetoothGatt;
	    }
	    
	    return null;
	}
	
	public ArrayList<BluetoothGatt> GetAllDeviceGatt()
	{
	    ArrayList<BluetoothGatt> list = new ArrayList<BluetoothGatt>();
	    synchronized (deviceInfo) {
		for(BleDeviceInfo devinfo : deviceInfo)
		{
		    list.add(devinfo.mBluetoothGatt);
		}
	    }
	    
	    return list;
	}
	
	public void SetDeviceGatt(String BluetoothDeviceAddress, BluetoothGatt BluetoothGatt)
	{
	    BleDeviceInfo devinfo = GetDeviceInfo(BluetoothDeviceAddress);
	    if(devinfo != null)
	    {
		devinfo.mBluetoothGatt = BluetoothGatt;
	    }
	}
	
	public void SetDeviceConnectionState(BluetoothGatt BluetoothGatt, int ConnectionState)
	{
	    BleDeviceInfo devinfo = GetDeviceInfo(BluetoothGatt);
	    if(devinfo != null)
	    {
		devinfo.mConnectionState = ConnectionState;
	    }
	}	
	public void SetDeviceConnectionState(String BluetoothDeviceAddress, int ConnectionState)
	{
	    BleDeviceInfo devinfo = GetDeviceInfo(BluetoothDeviceAddress);
	    if(devinfo != null)
	    {
		devinfo.mConnectionState = ConnectionState;
	    }
	}	
	public int GetDeviceConnectionState(String BluetoothDeviceAddress)
	{
	    BleDeviceInfo devinfo = GetDeviceInfo(BluetoothDeviceAddress);
	    if(devinfo != null)
	    {
		return devinfo.mConnectionState;
	    }
	    
	    return STATE_DISCONNECTED;
	}
    }
    
    private BluetoothDeviceInfoPool DevicePool = new BluetoothDeviceInfoPool();
    
    // Implements callback methods for GATT events that the app cares about.  For example,
    // connection change and services discovered.
    private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            String intentAction;
            Log.d(TAG, "onConnectionStateChange status = " + status + " newState = " + newState);
            if (newState == BluetoothProfile.STATE_CONNECTED) 
            {
                intentAction = ACTION_GATT_CONNECTED;
                DevicePool.SetDeviceConnectionState(gatt, STATE_CONNECTED);
                String adress = DevicePool.GetDeviceAddress(gatt);
                if(adress == null)
                {
                    Log.e(TAG, "BluetoothGattCallback: onConnectionStateChange Bug here, we get a gatt but not address");
                }
                broadcastUpdate(intentAction, adress);
                Log.i(TAG, "Connected to GATT server.");
                // Attempts to discover services after successful connection.
                boolean result = gatt.discoverServices();
                Log.i(TAG, "Attempting to start service discovery:" + result);

            } 
            else if (newState == BluetoothProfile.STATE_DISCONNECTED) 
            {
                intentAction = ACTION_GATT_DISCONNECTED;
                DevicePool.SetDeviceConnectionState(gatt, STATE_DISCONNECTED);
                Log.i(TAG, "Disconnected from GATT server.");
                String adress = DevicePool.GetDeviceAddress(gatt);
                if(adress == null)
                {
                    Log.e(TAG, "BluetoothGattCallback: onServicesDiscovered Bug here, we get a gatt but not address");
                }
                broadcastUpdate(intentAction, adress);
            }
        }
        
        @Override
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            
            Log.d(TAG, "onServicesDiscovered received: " + status);
            if (status == BluetoothGatt.GATT_SUCCESS) 
            {
                String adress = DevicePool.GetDeviceAddress(gatt);
                if(adress == null)
                {
                    Log.e(TAG, "BluetoothGattCallback: onServicesDiscovered Bug here, we get a gatt but not address");
                }
                broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED, adress);
            }
            else 
            {
                System.out.println("onServicesDiscovered received: " + status);
            }
        }

        @Override
        public void onCharacteristicRead(BluetoothGatt gatt,
                                         BluetoothGattCharacteristic characteristic,
                                         int status) 
        {
            Log.d(TAG, "onCharacteristicRead");
            if (status == BluetoothGatt.GATT_SUCCESS) 
            {
                String adress = DevicePool.GetDeviceAddress(gatt);
                if(adress == null)
                {
                    Log.e(TAG, "BluetoothGattCallback: onCharacteristicRead Bug here, we get a gatt but not address");
                }
                broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic, adress);
            }
        }

        @Override
        public void onCharacteristicChanged(BluetoothGatt gatt,
                                            BluetoothGattCharacteristic characteristic) 
        {
            Log.d(TAG, "onCharacteristicChanged");
            String adress = DevicePool.GetDeviceAddress(gatt);
            if(adress == null)
            {
                Log.e(TAG, "BluetoothGattCallback: onCharacteristicChanged Bug here, we get a gatt but not address");
            }
            broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic, adress);
        }

        public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) 
        {
            Log.d(TAG, "onDescriptorRead status = " + status);
        };

        public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) 
        {
            Log.d(TAG, "onDescriptorWrite status = " + status);
        };

        public void onReliableWriteCompleted(BluetoothGatt gatt, int status) 
        {
            Log.d(TAG, "onReliableWriteCompleted status = " + status);
        };

        public void onCharacteristicWrite(BluetoothGatt gatt,
                BluetoothGattCharacteristic characteristic, int status) 
        {
//            Log.d(TAG, "onCharacteristicWrite status = " + status);
        };
    };

//    private void broadcastUpdate(final String action) 
//    {
//        final Intent intent = new Intent(action);
//        sendBroadcast(intent);
//    }

    private void broadcastUpdate(final String action, final String BluetoothDeviceAddress) 
    {
        final Intent intent = new Intent(action);
        intent.putExtra(DEVICE_ADDRESS, BluetoothDeviceAddress);
        sendBroadcast(intent);
    }
    
    private void broadcastUpdate(final String action,
                                 final BluetoothGattCharacteristic characteristic,
                                 final String BluetoothDeviceAddress) {
        final Intent intent = new Intent(action);

        // This is special handling for the Heart Rate Measurement profile.  Data parsing is
        // carried out as per profile specifications:
        // http://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.heart_rate_measurement.xml
        if (UUID_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid())) 
        {
            int flag = characteristic.getProperties();
            int format = -1;
            if ((flag & 0x01) != 0) 
            {
                format = BluetoothGattCharacteristic.FORMAT_UINT16;
                Log.d(TAG, "Heart rate format UINT16.");
            } else 
            {
                format = BluetoothGattCharacteristic.FORMAT_UINT8;
                Log.d(TAG, "Heart rate format UINT8.");
            }
            final int heartRate = characteristic.getIntValue(format, 1);
            Log.d(TAG, String.format("Received heart rate: %d", heartRate));
            intent.putExtra(DEVICE_ADDRESS, BluetoothDeviceAddress);
            intent.putExtra(EXTRA_DATA, String.valueOf(heartRate));
        } else 
        {
            // For all other profiles, writes the data formatted in HEX.
            final byte[] data = characteristic.getValue();
            if (data != null && data.length > 0) 
            {
                intent.putExtra(DEVICE_ADDRESS, BluetoothDeviceAddress);
        	intent.putExtra(EXTRA_DATA, data);
            }
        }
        sendBroadcast(intent);
    }

    public class LocalBinder extends Binder {
        BluetoothLeServiceV2 getService() {
            return BluetoothLeServiceV2.this;
        }
    }

    @Override
    public IBinder onBind(Intent intent) 
    {
        Log.d(TAG, "ble onBind");
        return mBinder;
    }

    @Override
    public boolean onUnbind(Intent intent) 
    {
        // After using a given device, you should make sure that BluetoothGatt.close() is called
        // such that resources are cleaned up properly.  In this particular example, close() is
        // invoked when the UI is disconnected from the Service.
        close();
        Log.d(TAG, "ble onUnbind");
        return super.onUnbind(intent);
    }

    private final IBinder mBinder = new LocalBinder();

    /**
     * Initializes a reference to the local Bluetooth adapter.
     *
     * @return Return true if the initialization is successful.
     */
    public boolean initialize() 
    {
        // For API level 18 and above, get a reference to BluetoothAdapter through
        // BluetoothManager.
        if (mBluetoothManager == null) {
            mBluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
            if (mBluetoothManager == null) {
                Log.e(TAG, "Unable to initialize BluetoothManager.");
                return false;
            }
        }

        mBluetoothAdapter = mBluetoothManager.getAdapter();
        if (mBluetoothAdapter == null) {
            Log.e(TAG, "Unable to obtain a BluetoothAdapter.");
            return false;
        }

        return true;
    }

    /**
     * Connects to the GATT server hosted on the Bluetooth LE device.
     *
     * @param address The device address of the destination device.
     *
     * @return Return true if the connection is initiated successfully. The connection result
     *         is reported asynchronously through the
     *         {@code BluetoothGattCallback#onConnectionStateChange(android.bluetooth.BluetoothGatt, int, int)}
     *         callback.
     */
    public boolean connect(final String address) 
    {
        String intentAction;

        if (mBluetoothAdapter == null || address == null) {
            Log.w(TAG, "BluetoothAdapter not initialized or unspecified address.");
            Log.d(TAG, "adapter = " + mBluetoothAdapter.getName() + " address = " + address);
            return false;
        }

        // Previously connected device.  Try to reconnect.
        if(DevicePool.HasDevice(address))
        {
            BluetoothGatt gatt = DevicePool.GetDeviceGatt(address);
            if (gatt != null) {
        	Log.d(TAG, "Trying to use an existing mBluetoothGatt for connection.");
        	if (gatt.connect()) 
        	{
        	    DevicePool.SetDeviceConnectionState(address, STATE_CONNECTING);
        	    intentAction = ACTION_GATT_CONNECTED;
        	    broadcastUpdate(intentAction, address);
        	    return true;
        	} 
        	else 
        	{
        	    return false;
        	}
            }
        }

        final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address);
        if (device == null) {
            Log.w(TAG, "Device not found.  Unable to connect.");
            return false;
        }
        // We want to directly connect to the device, so we are setting the autoConnect
        // parameter to false.
        Log.d(TAG, "Trying to create a new connection.");
        BluetoothGatt BluetoothGatt = device.connectGatt(this, false, mGattCallback);
        DevicePool.AddDevice(address, BluetoothGatt);
        DevicePool.SetDeviceConnectionState(address, STATE_CONNECTING);
        
        System.out.println("device.getBondState=="+device.getBondState());
        return true;
    }

    /**
     * Disconnects an existing connection or cancel a pending connection. The disconnection result
     * is reported asynchronously through the
     * {@code BluetoothGattCallback#onConnectionStateChange(android.bluetooth.BluetoothGatt, int, int)}
     * callback.
     */
    public void disconnect() {
	ArrayList<BluetoothGatt> list = DevicePool.GetAllDeviceGatt();
	
	for(BluetoothGatt gatt: list)
	{
	    if (mBluetoothAdapter == null || gatt == null) 
	    {
		Log.w(TAG, "BluetoothAdapter not initialized");
		continue;
	    }	    	    
	    gatt.disconnect();
	    DevicePool.SetDeviceConnectionState(gatt, STATE_DISCONNECTING);
	}
    }
    
    public void disconnect(String address) {
	BluetoothGatt BluetoothGatt = DevicePool.GetDeviceGatt(address);
        if (mBluetoothAdapter == null || BluetoothGatt == null) 
        {
            Log.w(TAG, "BluetoothAdapter not initialized");
            return;
        }

        BluetoothGatt.disconnect();
        DevicePool.SetDeviceConnectionState(BluetoothGatt, STATE_DISCONNECTING);
    }
    /**
     * After using a given BLE device, the app must call this method to ensure resources are
     * released properly.
     */
    public void close() {
	ArrayList<BluetoothGatt> list = DevicePool.GetAllDeviceGatt();
	
	for(BluetoothGatt gatt: list)
	{
	    if (gatt == null) {
		return;
	    }
	    gatt.close();
	    DevicePool.DelDevice(gatt);
	}
	
    }
    public void close(String address) {
	BluetoothGatt BluetoothGatt = DevicePool.GetDeviceGatt(address);
        if (BluetoothGatt == null) {
            return;
        }
        BluetoothGatt.close();
        DevicePool.DelDevice(BluetoothGatt);
    }
    
    /**
     * Request a read on a given {@code BluetoothGattCharacteristic}. The read result is reported
     * asynchronously through the {@code BluetoothGattCallback#onCharacteristicRead(android.bluetooth.BluetoothGatt, android.bluetooth.BluetoothGattCharacteristic, int)}
     * callback.
     *
     * @param characteristic The characteristic to read from.
     */
    public void readCharacteristic(BluetoothGatt mBluetoothGatt, BluetoothGattCharacteristic characteristic) {
        if (mBluetoothAdapter == null || mBluetoothGatt == null) {
            Log.w(TAG, "BluetoothAdapter not initialized");
            return;
        }
        mBluetoothGatt.readCharacteristic(characteristic);
    }

    public void writeCharacteristic(String address, BluetoothGattCharacteristic characteristic) {	
	BluetoothGatt mBluetoothGatt = DevicePool.GetDeviceGatt(address);
        if (mBluetoothAdapter == null || mBluetoothGatt == null) {
            Log.w(TAG, "BluetoothAdapter not initialized");
            return;
        }
        mBluetoothGatt.writeCharacteristic(characteristic);
    }


    /**
     * Enables or disables notification on a give characteristic.
     *
     * @param characteristic Characteristic to act on.
     * @param enabled If true, enable notification.  False otherwise.
     */
    public void setCharacteristicNotification(String address, BluetoothGattCharacteristic characteristic,
                                              boolean enabled)
    {
	//our dev also use heart rate measurement descriptor..but uuid is a fucking value
	String read_uuidString = "f000aa63-0451-4000-b000-000000000000";
	
        if (mBluetoothAdapter == null || address == null) {
            Log.w(TAG, "BluetoothAdapter not initialized");
            return;
        }
        BluetoothGatt mBluetoothGatt = DevicePool.GetDeviceGatt(address);
        boolean res = mBluetoothGatt.setCharacteristicNotification(characteristic, enabled);
        Log.d(TAG, "ble setCharacteristicNotification result = " + res);

        // This is specific to Heart Rate Measurement.
//        if (UUID_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid()) || read_uuidString.equals(characteristic.getUuid()))
        {
            BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
                    UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
            descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
            mBluetoothGatt.writeDescriptor(descriptor);
        }
    }

    /**
     * Retrieves a list of supported GATT services on the connected device. This should be
     * invoked only after {@code BluetoothGatt#discoverServices()} completes successfully.
     *
     * @return A {@code List} of supported services.
     */
    public List<BluetoothGattService> getSupportedGattServices(String address) {
	BluetoothGatt mBluetoothGatt  = DevicePool.GetDeviceGatt(address);
        if (mBluetoothGatt == null) return null;

        return mBluetoothGatt.getServices();
    }

    /**
     * Return ConnectionState
     * @return
     */
    public int getConnectionState(String address) {
        return DevicePool.GetDeviceConnectionState(address);
    }

}
